在 Elixir 中,流程預設是短暫的;一旦函數執行完畢,流程就會終止。要建立一個 持續且具狀態的流程,我們會使用遞迴來讓流程在迴圈中持續運作。
1. 尾調用優化(TCO)
如果函數的最後一個絕對動作是呼叫自身,Erlang 虛擬機(BEAM)就會執行 尾調用優化。它不會將新的框架加入堆疊,而是直接 跳回函數起始處 並帶入新的參數。
def factorial(n, acc), do: _fact(n-1, acc*n) # TCO
def factorial(n), do: n * factorial(n-1) # 非 TCO
def factorial(n), do: n * factorial(n-1) # 非 TCO
2. 持續狀態
狀態是透過將更新後的值作為參數傳遞給遞迴呼叫來維持。由於 TCO,這些參數會取代堆疊上的原始參數,而無需額外消耗記憶體,因此允許迴圈無限執行。
main.py
TERMINALbash — 80x24
> Ready. Click "Run" to execute.
>
QUESTION 1
What is the primary requirement for Tail-Call Optimization to occur?
The function must use the 'loop' keyword.
The recursive call must be the absolute final expression executed.
The function must be defined inside a module.
Arguments must be integers.
✅ Correct!
Correct. If any operation occurs after the call (like multiplication), the frame remains on the stack, leading to exhaustion.❌ Incorrect
TCO requires that no work remains after the recursive call so the VM can safely jump back to the start.QUESTION 2
Exercise: WorkingWithMultipleProcesses-1. Run the Spawn1 and Spawn4 code. See if you get comparable results. What is the observable difference?
Spawn1 crashes after one message; Spawn4 stays alive.
Spawn1 is faster than Spawn4.
Spawn4 uses more memory over time.
There is no observable difference.
✅ Correct!
Reference Answer: When running spawn1.exs, the process handles one message and dies. In spawn4.exs, the process recurses, remaining in the process list and capable of responding to subsequent messages.❌ Incorrect
Check the process lifecycle. One-off processes terminate immediately after their function block finishes.QUESTION 3
In pmap, we assign 'self' to 'me' before spawning. Why?
To make the code more readable.
Because 'self' inside a spawn block refers to the child process PID.
To bypass the Pin operator requirements.
✅ Correct!
Correct! If you used 'self' inside the child's block, the child would try to send the message to itself, not the parent.❌ Incorrect
Remember that 'self()' is a dynamic call; its value depends on which process is currently executing the code.QUESTION 4
Use spawn_link to start a child that sends a message and exits. If the parent sleeps for 500ms before receiving, what happens to the message?
The message is lost because the parent wasn't waiting.
The message is stored in the parent's mailbox until processed.
The parent crashes immediately.
✅ Correct!
Reference Answer: The message is placed in the mailbox. It does not matter that you weren't waiting; the BEAM buffers incoming messages. If trapping exits, you would receive both the custom message and the {:EXIT, pid, :normal} signal.❌ Incorrect
Elixir processes have mailboxes that act as buffers for incoming data.QUESTION 5
Is the order of replies in concurrent processes deterministic in theory?
Yes, they always arrive in spawn order.
No, it depends on the scheduler and execution time.
✅ Correct!
Correct. In practice, we use the Pin operator (^pid) to ensure we receive messages in a specific, deterministic order regardless of completion speed.❌ Incorrect
Concurrent execution is inherently non-deterministic. We must enforce order in our code logic.Case Study: Token Exchange Persistence
WorkingWithMultipleProcesses-2
You need to spawn two processes that receive unique tokens ('fred' and 'betty') and send them back. You must ensure the results are gathered in the correct order even if the processes finish at different times.
Q
How can you make the order of received tokens deterministic in practice?
Solution:
By capturing the PIDs of the spawned processes and using the Pin operator (^pid) in the receive block. This forces the parent to wait for the message from a specific PID before moving on to the next, regardless of which message is at the top of the mailbox.
By capturing the PIDs of the spawned processes and using the Pin operator (^pid) in the receive block. This forces the parent to wait for the message from a specific PID before moving on to the next, regardless of which message is at the top of the mailbox.
Q
Write the logical structure for a persistent 'Token' receiver using TCO.
Solution:
The receiver should be a function (e.g., 'loop') that contains a 'receive' block. After processing the token and sending it back, it must call 'loop()' as the final statement. This allows the process to handle multiple tokens over its lifetime without crashing the stack.
The receiver should be a function (e.g., 'loop') that contains a 'receive' block. After processing the token and sending it back, it must call 'loop()' as the final statement. This allows the process to handle multiple tokens over its lifetime without crashing the stack.